#!/usr/bin/env python
# -*-  coding: utf-8 -*-
#   
#   Gmail Backup GUI (graphical user interface)
#   
#   Copyright © 2008, 2009, 2010 Jan Svec <honza.svec@gmail.com> and Filip Jurcicek <filip.jurcicek@gmail.com>
#   
#   This file is part of Gmail Backup.
#
#   Gmail Backup is free software: you can redistribute it and/or modify it
#   under the terms of the GNU General Public License as published by the Free
#   Software Foundation, either version 3 of the License, or (at your option)
#   any later version.
#
#   Gmail Backup is distributed in the hope that it will be useful, but WITHOUT
#   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
#   more details.
#
#   You should have received a copy of the GNU General Public License along
#   with Gmail Backup.  If not, see <http://www.gnu.org/licenses/
#
#   See LICENSE file for license details
#
#   With significant refactorings by Jesper Hertel <jesper.hertel@gmail.com> (JH) 
#   in 2013.


# Standard Python libraries.
import os
import sys
import time
import imaplib
import locale
import translation

# wxPython
import wx.html
import wx.lib.newevent

# Project modules.
import gmb  # Gmail Backup main module.
import InterruptableThread 
import ThreadedGmailBackup
import gmail_backup_gui_settings

from gmail_backup_gui_settings import saveSettingsToFile, loadSettingsFromFileOrUseEmptySettings
from InterruptableThread import ThreadDoesNotExistError, ThreadIsNotActiveError


_submoduleListForVersioning = [gmb, InterruptableThread, ThreadedGmailBackup, gmail_backup_gui_settings]
import versioning; _revisionInfo = versioning.getRevisionInfoFromTagsAndSubModules('$Revision$', '$Date$', _submoduleListForVersioning)

GMB_GUI_REVISION = _revisionInfo.revisionNumber 
GMB_GUI_DATE_ISO_FORMAT = _revisionInfo.isoDate 

WINDOW_TITLE_IDLE = _('Gmail Backup')
WINDOW_TITLE_WORKING = _('(%.1f%%) Gmail Backup')


(UpdateLogEvent, EVT_UPDATE_LOG) = wx.lib.newevent.NewEvent()


class GmailBackupGUINotifier(gmb.GmailBackupConsoleNotifier):
    
    def __init__(self, mainwindow):
        super(GmailBackupGUINotifier, self).__init__()
        self.mainWindow = mainwindow
        self.lastUpdateInSecondsSinceTheEpoch = 0  # Set last update to the system Epoch (normally January 1st, 1970).


    def _createPossibleUpdateEvent(self, logMessage, forceUpdate=False):
        nowInSecondsSinceTheEpoch = time.time()
        oneSecondOrMoreHasPassedSinceLastUpdate = \
            nowInSecondsSinceTheEpoch - self.lastUpdateInSecondsSinceTheEpoch >= 1
        if oneSecondOrMoreHasPassedSinceLastUpdate or forceUpdate:
            # More than one second has passed since last update and/or forceUpdate is True.
            self.lastUpdateInSecondsSinceTheEpoch = nowInSecondsSinceTheEpoch
            event = UpdateLogEvent(logMessage = logMessage, 
                                   speedInKilobytesPerSecond = self.getCurrentAverageSpeedInKilobytesPerSecond(),
                                   totalMegabytes = self.getTotalProcessedInMegabytes(), 
                                   percentage = self.getPercentageCompleted())
        else:
            # No update.
            event = None

        return event

    def _perhapsPostUpdateEvent(self, message, forceUpdate):
        event = self._createPossibleUpdateEvent(message, forceUpdate)
        if event is not None:
            wx.PostEvent(self.mainWindow, event)

    def updatePrint(self, message):
        self._perhapsPostUpdateEvent(message, forceUpdate=True)

    def updatePrintTypeOver(self, message):
        pass

    def updateSpeedDisplay(self):
        self._perhapsPostUpdateEvent(message=None, forceUpdate=False)


BACKUP_FOLDER_TOOLTIP = _(
'''You can use following forms:

  directory
  filename.zip
  directory#template
  filename.zip#template

In the template you can use following variables, which will be substituted with values in the stored message:
  $YEAR, $MONTH, $DAY
  $HOUR, $MINUTE, $SECOND
  $FROM, $SUBJ, $SUBJECT

You can write them either as $YEAR or ${YEAR}.''')


PROPORTION_NOT_CHANGEABLE = 0

class GmailBackupMainPanel(wx.Panel):

    def __init__(self, *args, **kwargs):
        '''
        Initialize panel before it is shown.
        '''

        # Call the constructor of the super class.
        wx.Panel.__init__(self, *args, **kwargs)

        # Set the background color of the panel.
        self.setBackgroundColor()

        # Create a BoxSizer with all the controls.
        totalBoxSizer = self.createTotalBoxSizerWithAllControls()

        # Set up event bindings for the controls.
        self.bindEventsToControls()

        # Set the Backup button as the default button.
        self.backupButton.SetDefault()

        # Set the sizer for the panel to be totalBoxSizer.
        self.SetSizer(totalBoxSizer)  # This implicitly calls self.SetAutoLayout(True).

        # Adjust the *panel* (self) to fit the BoxSizer with all the controls.
        totalBoxSizer.Fit(self)
        
        #self.Refresh()  # JH: Why? Disabled as I can find no explanation for this. The window isn't even being displayed yet.

        # Center the panel in the parent window.
        self.Centre();

        # Set up timer. It won't be active until we call its Start() method.
        self.setUpTimer()
        
        # Signal no current worker.
        self.interruptibleWorkerThread = None  
        
        self._clearLocale()

        # Put all controls and window title in the "no process is running" state.
        self.putControlsAndWindowTitleInIdleState()

        self.notifier = GmailBackupGUINotifier(self)

        self.loadAndApplySettings()


    def createGridBagSizerWithAllInputFields(self):

        # Create all controls.
        self.createInputControls()

        # Create the static texts.
        staticTextGmailLogin = wx.StaticText(self, wx.ID_ANY, _("Gmail login:\n(full email address)"))
        staticTextGmailPassword = wx.StaticText(self, wx.ID_ANY, _("Gmail password:"))
        staticTextBackupFolder = wx.StaticText(self, wx.ID_ANY, _("Backup folder:"))
        staticTextSince = wx.StaticText(self, wx.ID_ANY, _("Since date:"))
        staticTextBefore = wx.StaticText(self, wx.ID_ANY, _("Before date:"))

        # Create an empty GridBagSizer.
        inputFieldsGridBagSizer = wx.GridBagSizer(vgap=15, hgap=15)

        # Add line 1
        inputFieldsGridBagSizer.Add(staticTextGmailLogin, (1, 1))
        inputFieldsGridBagSizer.Add(self.loginTextCtrl, (1, 2), wx.DefaultSpan, wx.EXPAND)
        
        # Add line 2
        inputFieldsGridBagSizer.Add(staticTextGmailPassword, (2, 1))
        inputFieldsGridBagSizer.Add(self.passwordTextCtrl, (2, 2), wx.DefaultSpan, wx.EXPAND)
        
        # Add line 3
        inputFieldsGridBagSizer.Add(staticTextBackupFolder, (3, 1))
        inputFieldsGridBagSizer.Add(self.folderAndFileTemplateTextCtrl, (3, 2), wx.DefaultSpan, wx.EXPAND)
        inputFieldsGridBagSizer.Add(self.selectFolderButton, (3, 3))
        
        # Add line 4
        inputFieldsGridBagSizer.Add(staticTextSince, (4, 1))
        inputFieldsGridBagSizer.Add(self.sinceDatePickerCtrl, (4, 2), wx.DefaultSpan, wx.EXPAND)
        inputFieldsGridBagSizer.Add(self.onlyNewestCheckBox, (4, 3), (2, 1), wx.ALIGN_CENTER_VERTICAL)  # (2, 1) means span two rows, this and the following.
        
        # Add line 5
        inputFieldsGridBagSizer.Add(staticTextBefore, (5, 1))
        inputFieldsGridBagSizer.Add(self.beforeDatePickerCtrl, (5, 2), wx.DefaultSpan, wx.EXPAND)

        return inputFieldsGridBagSizer


    def createButtonsForButtonRow(self):
        self.backupButton = wx.Button(self, wx.ID_OK, _('&Backup'))
        self.restoreButton = wx.Button(self, wx.ID_ANY, _("&Restore"))
        self.stopButton = wx.Button(self, wx.ID_ANY, _("&Stop"))
        self.newVersionButton = wx.Button(self, wx.ID_ANY, _("New Versions"))
        self.exitButton = wx.Button(self, wx.ID_EXIT, _('Quit'))

    COMMON_INPUT_FIELD_WIDTH = 300

    def createPasswordField(self):
        self.passwordTextCtrl = wx.TextCtrl(self, wx.ID_ANY, size=(self.COMMON_INPUT_FIELD_WIDTH, -1), style=wx.TE_PASSWORD)

    def createLoginField(self):
        self.loginTextCtrl = wx.TextCtrl(self, wx.ID_ANY, size=(self.COMMON_INPUT_FIELD_WIDTH, -1))

    def createFolderButton(self):
        self.selectFolderButton = wx.Button(self, wx.ID_ANY, _("&Directory"))

    def createBackupFolderField(self):
        self.folderAndFileTemplateTextCtrl = wx.TextCtrl(self, wx.ID_ANY, size=(self.COMMON_INPUT_FIELD_WIDTH, -1))
        self.folderAndFileTemplateTextCtrl.SetToolTipString(BACKUP_FOLDER_TOOLTIP)

    def createBeforeDateField(self):
        self.beforeDatePickerCtrl = wx.DatePickerCtrl(self, wx.ID_ANY, size=(self.COMMON_INPUT_FIELD_WIDTH, -1))
        self.beforeDatePickerCtrl.Disable()

    def createSinceDateField(self):
        self.sinceDatePickerCtrl = wx.DatePickerCtrl(self, wx.ID_ANY, size=(self.COMMON_INPUT_FIELD_WIDTH, -1))
        self.sinceDatePickerCtrl.Disable()

    def createOnlyNewestCheckBox(self):
        self.onlyNewestCheckBox = wx.CheckBox(self, wx.ID_ANY, label=self.onlyNewestLabel())
        self.onlyNewestCheckBox.SetValue(True)

    def createProgressGauge(self):
        self.progressGauge = wx.Gauge(self, wx.ID_ANY, size=(self.COMMON_INPUT_FIELD_WIDTH, 10), style=wx.GA_HORIZONTAL)
        self.progressGauge.SetRange(100)

    def createProgressText(self):
        self.progressMessageStaticText = wx.StaticText(self, wx.ID_ANY, size=(self.COMMON_INPUT_FIELD_WIDTH, -1), style=wx.ALIGN_LEFT)

    def createLogBox(self):
        self.logBoxTextCtrl = wx.TextCtrl(
            self, 
            wx.ID_ANY, 
            size=(-1, 150), 
            style = 
                    wx.TE_MULTILINE 
                  | wx.TE_WORDWRAP 
                  | wx.TE_READONLY 
                  | wx.HSCROLL 
                  | wx.VSCROLL 
                  | wx.ALWAYS_SHOW_SB)

    def createLogo(self):
        logoStaticBitmap = wx.StaticBitmap(self, wx.ID_ANY, wx.Bitmap(os.path.join(os.path.dirname(sys.argv[0]), 'gmb.gif')))
        return logoStaticBitmap

    def createRevisionText(self):
        revisionStaticText = wx.StaticText(self, wx.ID_ANY, _("revision %s (%s)") % (GMB_GUI_REVISION, GMB_GUI_DATE_ISO_FORMAT))
        return revisionStaticText 

    def createButtonRowBoxSizer(self):

        self.createButtonsForButtonRow()

        buttonRowBoxSizer = wx.BoxSizer(wx.HORIZONTAL)
        
        sizerFlags = wx.SizerFlags().Center()  # Start with common flags.
        sizerFlags.Border(wx.ALL, 5)  # Add 5 pixels border all around in the sizerFlags object.
        buttonRowBoxSizer.AddF(self.backupButton, sizerFlags)
        buttonRowBoxSizer.AddF(self.restoreButton, sizerFlags)
        buttonRowBoxSizer.AddF(self.stopButton, sizerFlags)
        buttonRowBoxSizer.AddF(self.newVersionButton, sizerFlags)
        
        sizerFlags.Border(wx.LEFT, 55)  # *Change* left border to 55 pixels in the sizerFlags object.
        buttonRowBoxSizer.AddF(self.exitButton, sizerFlags)
        
        return buttonRowBoxSizer


    def setBackgroundColor(self):
        colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
        self.SetBackgroundColour(colour)


    def createProgressControlsBoxSizer(self):

        self.createProgressControls()

        progressControlsBoxSizer = wx.BoxSizer(wx.HORIZONTAL)
        
        if False: # Old non-SizerFlags version.
            progressControlsBoxSizer.Add(self.progressMessageStaticText, PROPORTION_NOT_CHANGEABLE, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL, 0)
            progressControlsBoxSizer.Add(self.progressGauge, PROPORTION_NOT_CHANGEABLE, wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 0)
        else:  # New SizerFlags version.
            progressControlsBoxSizer.AddF(self.progressMessageStaticText, wx.SizerFlags().Proportion(1).Expand())
            progressControlsBoxSizer.AddF(self.progressGauge, wx.SizerFlags().Proportion(1).Expand())
            
        return progressControlsBoxSizer


    def createLogAndProgressBoxSizer(self):
        
        self.createLogBox()
        progressControlsBoxSizer = self.createProgressControlsBoxSizer()

        logAndProgressBoxSizer = wx.BoxSizer(wx.VERTICAL)
        
        if False:
            logAndProgressBoxSizer.Add(self.logBoxTextCtrl, PROPORTION_NOT_CHANGEABLE, wx.EXPAND, 0)
            logAndProgressBoxSizer.Add(progressControlsBoxSizer, PROPORTION_NOT_CHANGEABLE, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 0)
        else:
            logAndProgressBoxSizer.AddF(self.logBoxTextCtrl, wx.SizerFlags().Expand())
            logAndProgressBoxSizer.AddF(progressControlsBoxSizer, wx.SizerFlags())

        return logAndProgressBoxSizer


    def createSeparatorLine(self):
        separatorLine = wx.StaticLine(self, wx.ID_ANY, size=(20, -1), style=wx.LI_HORIZONTAL)
        return separatorLine


    def createTotalBoxSizerWithAllControls(self):

        # Create logo graphics.
        gmailBackupLogo = self.createLogo()
        
        # Create revision text.
        revisionStaticText = self.createRevisionText()

        # Create a GridBagSizer with all input fields.
        inputFieldsGridBagSizer = self.createGridBagSizerWithAllInputFields()
        
        # Create a static horizontal separator line.
        separatorLine1 = self.createSeparatorLine()
        
        # Create a BoxSizer with the log box and progress controls.
        logAndProgressBoxSizer = self.createLogAndProgressBoxSizer()

        # Create a static horizontal separator line.
        separatorLine2 = self.createSeparatorLine()

        # Create a BoxSizer with all the bottom row buttons.
        buttonRowBoxSizer = self.createButtonRowBoxSizer()

        # Create a BoxSizer.
        boxSizerAll = wx.BoxSizer(wx.VERTICAL)
        
        # Put all the parts in the BoxSizer.
        boxSizerAll.Add(gmailBackupLogo, 0, wx.ALIGN_CENTER | wx.TOP, border=25)
        boxSizerAll.Add(revisionStaticText, 0, wx.ALIGN_CENTER | wx.BOTTOM, border=0)
        boxSizerAll.Add(inputFieldsGridBagSizer, 0, wx.ALIGN_CENTER | wx.ALL, border=5)
        boxSizerAll.Add(separatorLine1, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.TOP, border=5)
        boxSizerAll.Add(logAndProgressBoxSizer, 0, wx.GROW | wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, border=15)
        boxSizerAll.Add(separatorLine2, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.TOP, border=5)
        boxSizerAll.Add(buttonRowBoxSizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, border=15)

        return boxSizerAll


    def bindEventsToControls(self):
        self.selectFolderButton.Bind(wx.EVT_BUTTON, self.onSelectFolderPressed)
        self.backupButton.Bind(wx.EVT_BUTTON, self.onBackupButtonPressed)
        self.restoreButton.Bind(wx.EVT_BUTTON, self.onRestoreButtonPressed)
        self.stopButton.Bind(wx.EVT_BUTTON, self.onStopSelected)
        self.newVersionButton.Bind(wx.EVT_BUTTON, self.onNewVersionSelected)
        self.exitButton.Bind(wx.EVT_BUTTON, self.onExitSelected)
        self.onlyNewestCheckBox.Bind(wx.EVT_CHECKBOX, self.onOnlyNewestChanged)
        self.Bind(EVT_UPDATE_LOG, self.onLogAppend)
        # btnAbout.Bind(wx.EVT_BUTTON, self.onAboutSelected)
        # btnHelp.Bind(wx.EVT_BUTTON, self.onHelpSelected)



    def createProgressControls(self):
        self.createProgressText()
        self.createProgressGauge()

    def createInputControls(self):
        self.createLoginField()
        self.createPasswordField()
        self.createBackupFolderField()
        self.createFolderButton()
        self.createSinceDateField()
        self.createBeforeDateField()
        self.createOnlyNewestCheckBox()


    def setUpTimer(self):
        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.onTimer)

    def onlyNewestLabel(self):
        if os.name == 'posix':
            onlyNewestLabel = _("Newest\nemails\nonly")
        else:
            onlyNewestLabel = _("Newest emails only")
        return onlyNewestLabel

    def _clearLocale(self):
        self._prevLocale = locale.getlocale(locale.LC_TIME)
        locale.setlocale(locale.LC_TIME, 'C')  # use default (C) locale

    def _restoreLocale(self):
        locale.setlocale(locale.LC_TIME, self._prevLocale)


    def putControlsAndWindowTitleInRunningState(self):
        '''
        Put controls and the window title in the state that fits 
        a running process. 
        
        This means disabling most controls but enabling a few.
        '''
        
        self.loginTextCtrl.Disable()
        self.passwordTextCtrl.Disable()
        self.folderAndFileTemplateTextCtrl.Disable()
        self.selectFolderButton.Disable()
        self.sinceDatePickerCtrl.Disable()
        self.beforeDatePickerCtrl.Disable()
        self.onlyNewestCheckBox.Disable()
        self.backupButton.Disable()
        self.restoreButton.Disable()
        self.newVersionButton.Disable()
        self.exitButton.Disable()

        # Enable the Stop button so the user can stop the process.
        self.stopButton.Enable()
        
        # Disable/enable menu items.
        self.GetParent().enableStopMenuItem()
        self.GetParent().disableNewVersionMenuItem()

        # Enable progress controls.
        self.progressGauge.Enable()
        self.progressMessageStaticText.Enable()

        # Adjust window title.
        self.setWindowTitle(percentageCompleted=0)
        
        # Set focus in the log box.
        # This is to make keyboard accelerator keys for 
        # buttons and menus available.
        self.logBoxTextCtrl.SetFocus()


    def putControlsAndWindowTitleInIdleState(self):
        '''
        Put controls and window title in the state that fits the idle state
        (no running process).

        This means enabling most controls but disabling a few.
        '''

        self.loginTextCtrl.Enable()
        self.passwordTextCtrl.Enable()
        self.folderAndFileTemplateTextCtrl.Enable()
        self.selectFolderButton.Enable()
        self.onlyNewestCheckBox.Enable()
        self.enableOrDisableDatesDependingOnOnlyNewestValue()
        self.backupButton.Enable()
        self.restoreButton.Enable()
        self.newVersionButton.Enable()
        self.exitButton.Enable()

        # Disable the Stop button. This is because there is nothing to stop 
        # because the program is idle – no backup, restore or other longer 
        # process is running.
        self.stopButton.Disable()
        
        # Disable/enable menu items.
        self.GetParent().disableStopMenuItem()
        self.GetParent().enableNewVersionMenuItem()

        # Disable progress controls.
        self.progressGauge.Disable()
        self.progressMessageStaticText.Disable()
        
        # Set focus in the password field.
        self.passwordTextCtrl.SetFocus()
        
        # Adjust window title.
        self.setWindowTitle(percentageCompleted=None)
        

    def enableOrDisableDatesDependingOnOnlyNewestValue(self):
        if self.onlyNewestCheckBox.GetValue():
            self.sinceDatePickerCtrl.Disable()
            self.beforeDatePickerCtrl.Disable()
        else:
            self.sinceDatePickerCtrl.Enable()
            self.beforeDatePickerCtrl.Enable()            


    def applyLoginSetting(self):
        self.loginTextCtrl.SetValue(self.settings["login"].strip()) # JH: Why strip()?

    def applyFolderSetting(self):
        self.folderAndFileTemplateTextCtrl.SetValue(self.settings["folder"].strip()) # JH: Why strip()?

    def applyOnlyNewestSetting(self):
        onlyNewest = self.settings["onlyNewest"].strip() == "True" # JH: Why strip()?
        self.onlyNewestCheckBox.SetValue(onlyNewest)
        self.enableOrDisableDatesDependingOnOnlyNewestValue()

    def applySinceSetting(self):
        try:
            self.sinceDatePickerCtrl.SetValue(self.convertDateInISOFormatToWxDateTimeObject(self.settings["since"]))
        except:
            pass

    def applyBeforeSetting(self):
        try:
            self.beforeDatePickerCtrl.SetValue(self.convertDateInISOFormatToWxDateTimeObject(self.settings["before"]))
        except:
            pass


    def applySettingsToControls(self):
        '''
        Set the controls according to the values in self.settings.
        '''
        self.applyLoginSetting()
        self.applyFolderSetting()
        self.applyOnlyNewestSetting()
        self.applySinceSetting()
        self.applyBeforeSetting()

    def readSettingsFromControls(self):
        '''
        Read all settings from the controls and store them in self.settings.
        
        Note that we do not save the password, as this would be unsafe.
        '''
        self.settings["login"] = self.loginTextCtrl.GetValue()
        self.settings["folder"] = self.folderAndFileTemplateTextCtrl.GetValue()
        self.settings["onlyNewest"] = str(self.onlyNewestCheckBox.GetValue()) # "True" or "False". JH: Why convert to a string?
        self.settings["since"] = str(self.sinceDatePickerCtrl.GetValue().FormatISODate()) # In ISO format. JH: Why str()? Doesn't FormatISODate() already return a string?
        self.settings["before"] = str(self.beforeDatePickerCtrl.GetValue().FormatISODate()) # In ISO format. JH: Why str()? Doesn't FormatISODate() already return a string?


    def loadAndApplySettings(self):
        self.settings = loadSettingsFromFileOrUseEmptySettings()
        self.applySettingsToControls()

    def readSettingsFromControlsAndSaveThem(self):
        self.readSettingsFromControls()
        saveSettingsToFile(self.settings)

    
    def convertDateInISOFormatToWxDateTimeObject(self, dateInISOFormat):
        wxDateTimeObject = wx.DateTime()
        wxDateTimeObject.ParseDate(dateInISOFormat.strip())
        return wxDateTimeObject


    def convertWxDateTimeObjectToDateYYYYMMDD(self, wxDateTimeObject):

        # JH: It's probably more straightforward to use the wxDateTimeObject.Format() 
        # function (http://docs.wxwidgets.org/2.8/wx_wxdatetime.html#wxdatetimeformat)
        # instead of the following.
        year = wxDateTimeObject.GetYear()
        month = wxDateTimeObject.GetMonth()+1 # Are month numbers in range 0..11? (JH: It's an enum. See http://docs.wxwidgets.org/trunk/classwx_date_time.html#a156e17eb15e3c16a7da36cd810ff9117).
        day = wxDateTimeObject.GetDay()
        dateYYYYMMDD = '%04d%02d%02d' % (year, month, day)
        return dateYYYYMMDD


    def convertWxDateTimeObjectToImap4InternalDateString(self, wxDateTimeObject):
        dateYYYYMMDD = self.convertWxDateTimeObjectToDateYYYYMMDD(wxDateTimeObject)
        secondsSinceTheEpoch = time.mktime(time.strptime(dateYYYYMMDD, '%Y%m%d'))
        imap4InternalDateString = imaplib.Time2Internaldate(secondsSinceTheEpoch)
        return imap4InternalDateString


    def getListOfInputErrorsForBackupOrRestore(self):
        '''
        Return a list of all input errors in the input fields if the user
        wants to do a backup or restore. If the error list is empty, the 
        input data is fine. If not, the backup or restore process cannot be run.
        '''
        errorList = []
        if not self.loginTextCtrl.GetValue().strip():
            errorList.append(_("Enter an email address which you want to backup."))
        if not self.passwordTextCtrl.GetValue().strip():
            errorList.append(_("Enter a password for your Gmail account."))
        if not self.folderAndFileTemplateTextCtrl.GetValue().strip():
            errorList.append(_("Enter folder where you want to store your emails."))
        return errorList


    def reportInputErrors(self, errorList):
        '''
        Report the given list of errors to the user.
        
        :param errorList:
            List of errors. Each error is a string.
        '''

        # Construct string of all error messages, with vertical spacing
        # (two newlines) between.
        errorListAsString = "\n\n".join(errorList)

        messageDialog = wx.MessageDialog(self, errorListAsString, _("Invalid input data"), style=wx.CANCEL | wx.ICON_ERROR)
        messageDialog.ShowModal()
        messageDialog.Destroy()


    def startThreadedGmailBackupMethodAndDisableControls(self, username, password, methodName, *args, **kwargs):
        '''
        Start running the method with the given methodName and with the 
        given parameters as a separate process in a separate thread.
        '''

        if not self.interruptibleWorkerThread:
            # Create an object of the threaded version of the GmailBackup class.
            threadedGmailBackupObject = ThreadedGmailBackup.ThreadedGmailBackup(username, password, self.notifier)

            # Get the method reference from the given name.
            method = getattr(threadedGmailBackupObject, methodName)
            
            # Start the process in a separate thread and store the
            # reference to the thread in self.interruptibleWorkerThread.
            self.interruptibleWorkerThread = method(*args, **kwargs)
            
            # The process has been started now and is probably still running 
            # in the thread self.interruptibleWorkerThread.
            
            # Set controls in a state corresponding to a running process
            # (enable Stop controls, disable starting of other tasks, etc.).
            self.putControlsAndWindowTitleInRunningState()
            
            # Start the timer to detect from time to time if the thread has finished.
            self.startTimer()
        else:
            # Another process is already running.
            self.notifier.notifyLog(_("Can't start new process, as another process is already running."))
            

    def startTimer(self):
        '''
        Start generating a timer event every 200 milliseconds. 
        
        When the timer event occurs, self.onTimer() is called.
        
        This is used to check if the thread self.interruptibleWorkerThread 
        has finished.
        '''
        self.timer.Start(200)


    def appendToLog(self, logMessage):
        if logMessage is not None:
            logMessageWithNewline = '%s\n' % logMessage
            self.logBoxTextCtrl.AppendText(logMessageWithNewline)


    def setGaugeProgress(self, percentageCompleted):
        if percentageCompleted is not None:
            self.progressGauge.SetValue(percentageCompleted)
        else:
            self.progressGauge.SetValue(0)


    def updateProgressMessage(self, totalMegabytes, speedInKilobytesPerSecond):
        progressMessage = _('Processed %.2f MB, speed %.1f KB/s') % (totalMegabytes, speedInKilobytesPerSecond)
        self.progressMessageStaticText.SetLabel(progressMessage)


    def setWindowTitle(self, percentageCompleted=None):
        '''
        Set the window title according to the current progress given by
        percentageCompleted.
        
        If percentageCompleted is None, the title is set as idle (normal 
        title).
        '''

        if percentageCompleted is not None:
            self.Parent.SetLabel(WINDOW_TITLE_WORKING % percentageCompleted)
        else:
            self.Parent.SetLabel(WINDOW_TITLE_IDLE)


    def whenWorkingThreadHasFinished(self):
        self.timer.Stop()
        self.interruptibleWorkerThread = None  # Signal that there is no current process.
        self.putControlsAndWindowTitleInIdleState()


    def onLogAppend(self, event):
        logMessage = event.logMessage
        percentageCompleted = event.percentage
        totalMegabytes = event.totalMegabytes
        speedInKilobytesPerSecond = event.speedInKilobytesPerSecond

        self.appendToLog(logMessage)
        self.setGaugeProgress(percentageCompleted)
        self.updateProgressMessage(totalMegabytes, speedInKilobytesPerSecond)
        self.setWindowTitle(percentageCompleted)


    def onSelectFolderPressed(self, event):
        '''User pressed the Directory button to select folder
        for the backup files. 
        
        Present the user with a folder browser dialog and put the result, 
        if any, in the textCtrolFolderTemplate input box.'''
        
        directoryPath = os.path.abspath(self.folderAndFileTemplateTextCtrl.GetValue().strip())
        
        directoryDialog = wx.DirDialog(self, _("Select a directory for Gmail Backup"), directoryPath, size = (400, 600))
        try:
            if directoryDialog.ShowModal() == wx.ID_OK:
                directoryPath = directoryDialog.GetPath()
                self.folderAndFileTemplateTextCtrl.SetValue(directoryPath)
        finally:
            directoryDialog.Destroy()


    def onOnlyNewestChanged(self, event):
        self.enableOrDisableDatesDependingOnOnlyNewestValue()


    def buildWhereList(self, sinceImap4InternalDateString, beforeImap4InternalDateString):
        whereList = ['ALL']
        if sinceImap4InternalDateString is not None:
            whereList.append('SINCE')
            whereList.append(sinceImap4InternalDateString)
        if beforeImap4InternalDateString is not None:
            whereList.append('BEFORE')
            whereList.append(beforeImap4InternalDateString)
        return whereList


    def getImap4InternalDateStringFromControl(self, datePickerControl):
        sinceWxDateTimeObject = datePickerControl.GetValue()
        if sinceWxDateTimeObject:
            sinceImap4InternalDateString = self.convertWxDateTimeObjectToImap4InternalDateString(sinceWxDateTimeObject)
        else:
            sinceImap4InternalDateString = None
        return sinceImap4InternalDateString

    def calculateImap4InternalDateStringFromControl(self, datePickerControl, onlyNewest):
        if onlyNewest:
            sinceImap4InternalDateString = None
        else:
            sinceImap4InternalDateString = self.getImap4InternalDateStringFromControl(datePickerControl)
        return sinceImap4InternalDateString

    def calculateSinceImap4InternalDateString(self, onlyNewest):
        sinceImap4InternalDateString = self.calculateImap4InternalDateStringFromControl(self.sinceDatePickerCtrl, onlyNewest)
        return sinceImap4InternalDateString

    def calculateBeforeImap4InternalDateString(self, onlyNewest):
        beforeImap4InternalDateString = self.calculateImap4InternalDateStringFromControl(self.beforeDatePickerCtrl, onlyNewest)
        return beforeImap4InternalDateString

    def onBackupButtonPressed(self, event):
        '''Called when the user has pressed the Backup button.'''

        errorList = self.getListOfInputErrorsForBackupOrRestore()

        if errorList:
            self.reportInputErrors(errorList)

        else:            
            username = self.loginTextCtrl.GetValue()
            password = self.passwordTextCtrl.GetValue()
            folderAndFileTemplate = self.folderAndFileTemplateTextCtrl.GetValue()  # String.
            onlyNewest = self.onlyNewestCheckBox.GetValue()  # bool.
            sinceImap4InternalDateString = self.calculateSinceImap4InternalDateString(onlyNewest)
            beforeImap4InternalDateString = self.calculateBeforeImap4InternalDateString(onlyNewest)

            # JH: The building of whereList below should be moved to GmailBackup.backup(),
            # and the datetime format used for parameters should be the neutral datetime.datetime.
            whereList = self.buildWhereList(sinceImap4InternalDateString, beforeImap4InternalDateString)
    
            # Start the backup task. 
            # This call will exit quickly, but the process will continue in another thread.
            self.startThreadedGmailBackupMethodAndDisableControls(username, password, "backup", folderAndFileTemplate, whereList, onlyNewest)
            
            # Save settings from controls into the settings file.
            self.readSettingsFromControlsAndSaveThem()


    def getDateYYYYMMDDFromControl(self, datePickerCtrl):
        wxDateTimeObject = datePickerCtrl.GetValue()
        if wxDateTimeObject:
            dateYYYYMMDD = self.convertWxDateTimeObjectToDateYYYYMMDD(wxDateTimeObject)
        else:
            dateYYYYMMDD = None
        return dateYYYYMMDD

    def getSinceDateYYYYMMDDFromControl(self):
        sinceDateYYYYMMDD = self.getDateYYYYMMDDFromControl(self.sinceDatePickerCtrl)
        return sinceDateYYYYMMDD

    def getBeforeDateYYYYMMDDFromControl(self):
        beforeDateYYYYMMDD = self.getDateYYYYMMDDFromControl(self.datePickerCtrlbefore)
        return beforeDateYYYYMMDD

    def onRestoreButtonPressed(self, event):

        errorList = self.getListOfInputErrorsForBackupOrRestore()

        if errorList:
            # Errors in input fields detected.
            # Report them.
            self.reportInputErrors(errorList)
    
        else:
            # No errors in input fields detected.
            # We can start.

            # Get username, password, folderAndFileTemplate and onlyNewest settings.
            username = self.loginTextCtrl.GetValue()
            password = self.passwordTextCtrl.GetValue()
            folderAndFileTemplate = self.folderAndFileTemplateTextCtrl.GetValue()
            onlyNewest = self.onlyNewestCheckBox.GetValue()  # Boolean

            # Calculate sinceDateYYYYMMDD and beforeDateYYYYMMDD.
            if onlyNewest:
                sinceDateYYYYMMDD = None
                beforeDateYYYYMMDD = None
            else:
                sinceDateYYYYMMDD = self.getSinceDateYYYYMMDDFromControl()
                beforeDateYYYYMMDD = self.getBeforeDateYYYYMMDDFromControl()

            # Start the restore task.
            # This call will exit quickly, but the process will continue in another thread.
            self.startThreadedGmailBackupMethodAndDisableControls(username, password, "restore", folderAndFileTemplate, sinceDateYYYYMMDD, beforeDateYYYYMMDD)

            # Save settings from controls into the settings file.
            self.readSettingsFromControlsAndSaveThem()


    def onNewVersionSelected(self, event):
        self.startThreadedGmailBackupMethodAndDisableControls(None, None, "checkForNewVersion")


    def onStopSelected(self, event):
        self.stopWorkerThreadIfRunning()


    def onTimer(self, event):
        if not self.interruptibleWorkerThread.isAlive():
            # The thread self.interruptibleWorkerThread is no more with us. It has ceased to be and has gone to meet its maker.
            self.whenWorkingThreadHasFinished()
    

    def workerThreadIsRunning(self):
        return self.interruptibleWorkerThread is not None and self.interruptibleWorkerThread.isAlive()  # Works due to shortcut evaluation.


    def stopWorkerThreadIfRunning(self):
        '''
        Stop the worker thread if it is running.
        
        JH: Does it finish immediately when stopped? I.e., will it 
        always have finished when this method call finishes? Or is 
        a signal just being sent?
        '''
        
        # Note that the thread can stop at any time, so it is no use 
        # checking beforehand if the thread is running.
        self.notifier.notifyLog(_("Interrupting ..."))
        if self.interruptibleWorkerThread is not None:
            try:
                self.interruptibleWorkerThread.stopThread()
            except (ThreadDoesNotExistError, ThreadIsNotActiveError):
                # The thread already finished and therefore does not need 
                # to be stopped. This is fine and is not a problem in any way.
                pass
        else:
            # No active thread. Nothing to stop. Everything is fine.
            pass



    def beforeExit(self):
        '''
        Do things that needs to be done before exiting the
        program. 
        
        Called when the parent window is closed or the user clicks
        Exit or selects Quit from the menu.
        '''
        self.stopWorkerThreadIfRunning()
        self.readSettingsFromControlsAndSaveThem()


    def onWindowClose(self, event):
        '''
        Called when the parent window closes.
        '''
        self.beforeExit()


    def onExitSelected(self, event):
        # Close the parent window to this panel. 
        # The parent is the GmailBackupMainWindow.
        # This will in turn call self.onWindowClose() above to stop any
        # running jobs and save settings.
        self.GetParent().Close()


class GmailBackupMainWindow(wx.Frame):

    # Menu item ID constants.
    MENU_ID_STOP = wx.NewId()
    MENU_ID_CHECK_NEW_VERSION = wx.NewId()
    MENU_ID_QUIT = wx.ID_EXIT  # Special wxWindows ID.
    MENU_ID_ABOUT = wx.ID_ABOUT  # Special wxWindows ID.

    def __init__(self, 
                 parent, 
                 ID, 
                 title, 
                 size = wx.DefaultSize, 
                 position = wx.DefaultPosition, 
                 style = (wx.DEFAULT_FRAME_STYLE )#^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX) # No resize, no maximize.
                        | wx.TAB_TRAVERSAL
                ):

        # Call the constructor of the super class.
        wx.Frame.__init__(self, parent, ID, title, position, size, style = style)

        # Set icon.
        self.setIcon()

        # Create a panel (to be placed later between the menu bar and the status bar) ...
        self.panel = GmailBackupMainPanel(self, wx.ID_ANY)

        # Add a menu.
        self.addMenuBar()
        
        # Create a status bar at the bottom of the frame.
        self.CreateStatusBar()

        # Now add the GmailBackupMainPanel inside a BoxSizer.
        boxSizer = wx.BoxSizer(wx.VERTICAL)
        boxSizer.AddF(self.panel, wx.SizerFlags().Expand()) # PROPORTION_NOT_CHANGEABLE, wx.EXPAND)

        #self.Refresh()  # JH: Why? I commented it out because there is no explanation for this.

        # Add the BoxSizer to the window (Frame).
        self.SetSizer(boxSizer)  # This implicitly also calls self.SetAutoLayout(True).
        
        # Resize the window (Frame) to fit the BoxSizer's minimum dimensions.
        boxSizer.Fit(self)

        # JH: Bind closing of window so settings will be saved even when 
        # the window is closed without pressing the Quit menu item or 
        # Exit button.
        self.Bind(wx.EVT_CLOSE, self.onWindowClose)

        # Center the window.
        self.Centre();
        
        
    def onWindowClose(self, event):
        self.panel.onWindowClose(event)
        self.Destroy()
        

    def setIcon(self):
        icon = wx.Icon(os.path.join(os.path.dirname(sys.argv[0]), 'gmb.ico'), wx.BITMAP_TYPE_ICO)
        self.SetIcon(icon)

    def addMenuItem(self, wxMenu, menuItemId, text, helpText, eventHandler):
        '''
        Add menu item and bind it to an eventHandler.
        
        :param wxMenu:
        :param menuItemId:
            The ID for the menu item. Must not crash with builtin wx IDs. Only use ID's larger than wx.ID_HIGHEST.
        :param text:
            Text to be displayed in the menu item. 
            Include "&" before accelerator keys, e.g. "&Quit". Use "&&" to add a real "&" glyph.
        :param helpText:
            Help text to be displayed in the status bar when mouse hovers over
            the menu item.
        :param eventHandler:
            The event handler method to call when item is selected.
        '''
        wxMenu.Append(menuItemId, text, helpText)
        self.Bind(wx.EVT_MENU, eventHandler, id=menuItemId)

    def addMenuSeparator(self, wxMenu):
        wxMenu.AppendSeparator()

    def addCommandsMenu(self):
        self.commandsMenu = wx.Menu()
        self.menuBar.Append(self.commandsMenu, _("&Commands"))
        self.addMenuItem(self.commandsMenu, self.MENU_ID_STOP, _("&Stop"), _("Stop backing up or restoring emails"), self.panel.onStopSelected)
        self.addMenuSeparator(self.commandsMenu)
        self.addMenuItem(self.commandsMenu, self.MENU_ID_QUIT, _("&Quit"), _("Terminate the program"), self.panel.onExitSelected)

        # Make "Stop" item disabled by default. 
        # It will only be enabled when a longer process is running.
        self.disableMenuItem(self.MENU_ID_STOP)

    def addHelpMenu(self):
        self.helpMenu = wx.Menu()
        self.menuBar.Append(self.helpMenu, _("&Help"))
        self.addMenuItem(self.helpMenu, self.MENU_ID_CHECK_NEW_VERSION, _("&Check new versions"), _("Check for new versions of this program"), self.panel.onNewVersionSelected)
        self.addMenuItem(self.helpMenu, self.MENU_ID_ABOUT, _("&About"), _("More information about this program"), self.onAboutSelected)

    def addMenuBar(self):
        self.menuBar = wx.MenuBar()
        self.SetMenuBar(self.menuBar)
        self.addCommandsMenu()
        self.addHelpMenu()


    def enableMenuItem(self, menuItemId, enable=True):
        '''
        Enable or disable the menu item with the ID menuItemId.
        
        :param enable:
            Boolean. Whether the item should be enabled (True) or disabled (False).
        '''
        if hasattr(self, "menuBar"): # We only do this if the menu has been created.
            self.menuBar.Enable(menuItemId, enable)

    def disableMenuItem(self, menuItemId):
        self.enableMenuItem(menuItemId, enable=False)

        
    def enableStopMenuItem(self, enable=True):
        '''
        Enable or disable the Stop menu item, if it has been created.
        
        :param enable:
            Boolean. Whether the item should be enabled (true) or disabled (false).
        '''
        self.enableMenuItem(self.MENU_ID_STOP, enable)

    def disableStopMenuItem(self):
        '''
        Disable the Stop menu item, if it has been created.
        '''
        self.enableStopMenuItem(enable=False)

        
    def enableNewVersionMenuItem(self, enable=True):
        '''
        Enable or disable the New version menu item, if it has been created.
        
        :param enable:
            Boolean. Whether the item should be enabled (true) or disabled (false).
        '''
        self.enableMenuItem(self.MENU_ID_CHECK_NEW_VERSION, enable)

    def disableNewVersionMenuItem(self):
        '''
        Disable the New version menu item, if it has been created.
        '''
        self.enableNewVersionMenuItem(enable=False)


    def showAboutBox(self):
        aboutBoxDialog = AboutBoxDialog(GMB_GUI_REVISION, GMB_GUI_DATE_ISO_FORMAT)
        aboutBoxDialog.ShowModal()
        aboutBoxDialog.Destroy()

    def onAboutSelected(self, event):
        self.showAboutBox()
        
    def onHelpSelected(self, event):
        '''
        JH: No effect. Help not implemented.
        '''
        pass

  
aboutTextHtml = _("""
<p align="center">
    <b>Gmail Backup</b> 
    <br>
    <br>
    revision %(revision)s (%(revisiondate)s)
</p>

<p>
The purpose of this program is to simplify 
the process of backing up emails from your Gmail 
account, restoring them, or migrating emails from one 
Gmail account to another.
</p>

<p>
Copyright (C) 2008-2011 by Jan Svec and Filip Jurcicek.
</p>

<p>Further development in 2013 by Jesper Hertel.</p>

<p>
See <a href="http://code.google.com/p/gmail-backup-com/">http://code.google.com/p/gmail-backup-com/</a>
</p>
""") 


class HtmlWindow(wx.html.HtmlWindow):
    
    def __init__(self, parent, windowId, size=(600,400)):
        wx.html.HtmlWindow.__init__(self, parent, windowId, size=size)
        if "gtk2" in wx.PlatformInfo:
            self.SetStandardFonts()


    def OnLinkClicked(self, link):
        wx.LaunchDefaultBrowser(link.GetHref())
        

class AboutBoxDialog(wx.Dialog):

    def __init__(self,  revisionNumber, isoDate):
    
        wx.Dialog.__init__(self, 
            None, 
            wx.ID_ANY, 
            _("About Gmail Backup"),
            style=
                wx.DEFAULT_DIALOG_STYLE
                | wx.THICK_FRAME
                | wx.RESIZE_BORDER
                | wx.TAB_TRAVERSAL)

        colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
        self.SetBackgroundColour(colour)
        
        verticalBoxSizer = wx.BoxSizer(wx.VERTICAL)

        htmlWindow = HtmlWindow(self, wx.ID_ANY, size=(400,200))
        versionDict = {
            "revision": revisionNumber,
            "revisiondate": isoDate 
        }
        htmlWindow.SetPage(aboutTextHtml % versionDict)
        internalRepresentation = htmlWindow.GetInternalRepresentation()
        htmlWindow.SetSize((internalRepresentation.GetWidth()+25, internalRepresentation.GetHeight()+20))
        self.SetClientSize(htmlWindow.GetSize())

        self.buttonOk = wx.Button(self, wx.ID_OK, _("OK"))
        verticalBoxSizer.Add(htmlWindow, PROPORTION_NOT_CHANGEABLE, wx.ALIGN_CENTER | wx.ALL, 15)
        verticalBoxSizer.Add(self.buttonOk, PROPORTION_NOT_CHANGEABLE, wx.ALIGN_CENTER | wx.BOTTOM, 15)

        self.SetSizer(verticalBoxSizer)
        self.SetAutoLayout(True)
        verticalBoxSizer.Fit(self)

        self.CentreOnParent(wx.BOTH)
        self.SetFocus()


def main():
    translation.installTranslation()
    applicationObject = wx.PySimpleApp()
    mainProgramWindow = GmailBackupMainWindow(None, wx.ID_ANY, WINDOW_TITLE_IDLE)
    applicationObject.SetTopWindow(mainProgramWindow)
    mainProgramWindow.Show()
    applicationObject.MainLoop()


#############################################################################
## Main program.
#############################################################################
if __name__ == "__main__":
    main()
    
